<?php
	/**
	 * Tuxxedo Software Engine
	 * =============================================================================
	 *
	 * @author		Kalle Sommer Nielsen <kalle@tuxxedo.net>
	 * @version		1.0
	 * @copyright		Tuxxedo Software Development 2006+
	 * @package		Engine
	 *
	 * =============================================================================
	 */

	defined('TUXXEDO') or exit;


	/**
	 * Datamanager driver API, a datamanger must extend this interface 
	 * in order to by loadable by the datamanger class.
	 *
	 * @author		Kalle Sommer Nielsen <kalle@tuxxedo.net>
	 * @version		1.0
	 * @package		Engine
	 */
	interface Tuxxedo_Datamanger_API
	{
		/**
		 * Constructor for the current datamanager, this 
		 * can be used to either create a datamanager based 
		 * on a certain record determined by the passed identifier 
		 * or as a clean datamanager to insert a new record.
		 *
		 * @param	mixed			The unique identifier to send to the datamanager
		 *
		 * @throws	Tuxxedo_Exception	Throws an exception if the unique identifier sent to the datamanager was invalid
		 */
		public function __construct($identifier = NULL);
	}

	/**
	 * Abstract datamanager class
	 *
	 * Every datamanager class must extend this class in order to be loadable and to 
	 * comply with the datamanager API. This also contains the factory method used 
	 * to instanciate a new datamanager instance.
	 *
	 * @author		Kalle Sommer Nielsen <kalle@tuxxedo.net>
	 * @version		1.0
	 * @package		Engine
	 */
	abstract class Tuxxedo_Datamanager implements Tuxxedo_Datamanger_API
	{
		/**
		 * Indicates that a field is required
		 *
		 * @var		integer
		 */
		const FIELD_REQUIRED			= 1;

		/**
		 * Indicates that a field is optional
		 *
		 * @var		integer
		 */
		const FIELD_OPTIONAL			= 2;

		/**
		 * Indicates that a field is protected
		 *
		 * @var		integer
		 */
		const FIELD_PROTECTED			= 3;

		/**
		 * Validation constant, numeric value
		 *
		 * @var		integer
		 */
		const VALIDATE_NUMERIC			= TYPE_NUMERIC;

		/**
		 * Validation constant, string value
		 *
		 * @var		integer
		 */
		const VALIDATE_STRING			= TYPE_STRING;

		/**
		 * Validation constant, email value
		 *
		 * @var		integer
		 */
		const VALIDATE_EMAIL			= TYPE_EMAIL;

		/**
		 * Validation constant, boolean value
		 *
		 * @var		integer
		 */
		const VALIDATE_BOOLEAN			= TYPE_BOOLEAN;

		/**
		 * Validation constant, callback
		 *
		 * @var		integer
		 */
		const VALIDATE_CALLBACK			= TYPE_CALLBACK;


		/**
		 * Private instance to the Tuxxedo registry
		 *
		 * @var		Tuxxedo
		 */
		protected $tuxxedo;

		/**
		 * Table name, set by the datamanager
		 *
		 * @var		array
		 */
		protected $tablename;

		/**
		 * Identifier name, set by the datamanager
		 *
		 * @var		array
		 */
		protected $idname;

		/**
		 * Identifier, if any
		 *
		 * @var		array
		 */
		protected $identifier;

		/**
		 * The original data if instanciated by an identifier
		 *
		 * @var		array
		 */
		protected $data				= Array();

		/**
		 * Current data thats been set via the set method
		 *
		 * @var		array
		 */
		protected $userdata			= Array();

		/**
		 * List of loaded datamanagers used for caching in the 
		 * special required cases where more than one driver 
		 * have to be loaded
		 *
		 * @var		array
		 */
		protected static $loaded_datamanagers 	= Array();

		/**
		 * List of fields that had one or more errors and therefore 
		 * could not be saved
		 *
		 * @var		array
		 */
		protected $invalid_fields		= Array();


		/**
		 * Constructs a new datamanger instance
		 *
		 * @param	string				Datamanger name
		 * @param	mixed				An identifier to send to the datamanager to load default data upon instanciating it
		 * @param	boolean				Load from datastore if the datamanager supports it, or from the database?
		 * @return	Tuxxedo_Datamanager		Returns a new database instance
		 *
		 * @throws	Tuxxedo_Basic_Exception		Throws a basic exception if loading of a datamanger should fail for some reason
		 *
		 * @note	If loading from the datastore, then only the data in the cache will be loaded, meaning 
		 * 		that not all the data that exists in the database may be available.
		 */
		final public static function factory($datamanager, $identifier = NULL, $cached = false)
		{
			$datamanager 	= (string) $datamanager;
			$path		= TUXXEDO_DIR . '/includes/datamanagers/dm_' . $datamanager . '.php';
			$class		= 'Tuxxedo_Datamanager_API_' . $datamanager;

			if(in_array($datamanager, self::$loaded_datamanagers))
			{
				return(new $class($identifier, $cached));
			}

			if(!is_file($path))
			{
				throw new Tuxxedo_Basic_Exception('Unable to find datamanager driver file for \'%s\'', $datamanager);
			}

			require($path);

			if(!class_exists($class))
			{
				throw new Tuxxedo_Basic_Exception('Corrupt datamanager driver, driver class not found for \'%s\'', $datamanager);
			}

			if(!is_subclass_of($class, __CLASS__))
			{
				throw new Tuxxedo_Basic_Exception('Corrupt datamanager driver, driver class does not follow the driver specification');
			}

			self::$loaded_datamanagers[] = $datamanager;

			return(new $class($identifier, $cached));
		}

		/**
		 * Gets a list over invalid fields, this is only populated 
		 * if an attempt to saving a datamanager have failed
		 *
		 * @return	array				Returns a list of those fields that failed validation
		 */
		public function getInvalidFields()
		{
			return($this->invalid_fields);
		}

		/**
		 * Updates a field
		 *
		 * @param	string				The field to update
		 * @param	mixed				The field value
		 * @return	void				No value is returned
		 */
		public function set($field, $value)
		{
			$this->userdata[strtolower($field)] = $value;
		}

		/**
		 * Gets a field
		 *
		 * @param	string				The field to get, if this value is NULL then all the backend data will be returned
		 * @return	mixed				Returns the field value, and NULL if the field is non existant (set)
		 */
		public function get($field = NULL)
		{
			if(is_null($field))
			{
				return($this->data);
			}
			elseif(isset($this->userdata[$field]))
			{
				return($this->userdata[$field]);
			}
			elseif(isset($this->data[$field]))
			{
				return($this->data[$field]);
			}
		}

		/**
		 * Validation method, validates the supplied user data 
		 *
		 * @return	boolean				Returns true if the data is valid, otherwise false
		 */
		public function validate()
		{
			$this->invalid_fields = Array();

			if(!sizeof($this->userdata))
			{
				return(true);
			}

			foreach($this->fields as $field => $properties)
			{
				switch($properties['type'])
				{
					case(self::FIELD_REQUIRED):
					{
						if(!isset($this->userdata[$field]))
						{
							if(!isset($this->data[$field]))
							{
								$this->invalid_fields[] = $field;
							}

							continue 2;
						}
					}
					break;
					case(self::FIELD_PROTECTED):
					{
						if(isset($this->userdata[$field]))
						{
							$this->invalid_fields[] = $field;
						}

						if(isset($properties['default']))
						{
							$this->data[$field] = $properties['default'];
						}
						elseif(isset($properties['callback']) && is_callable($properties['callback']))
						{
							if(isset($properties['parameters']))
							{
								$this->data[$field] = call_user_func_array($properties['callback'], array_merge(Array($this->tuxxedo), $properties['parameters']));
							}
							else
							{
								$this->data[$field] = call_user_func($properties['callback'], $this->tuxxedo);
							}
						}

						continue 2;
					}
					break;
				}

				if(isset($properties['default']))
				{
					$this->data[$field] = $properties['default'];
				}

				if($properties['validation'] == self::VALIDATE_CALLBACK && isset($properties['callback']))
				{
					if(!$this->tuxxedo->filter->validate($this->userdata[$field], $properties['callback']))
					{
						$this->invalid_fields[] = $field;

						continue;
					}
				}
				else
				{
					$filtered = $this->tuxxedo->filter->user($this->userdata[$field], $properties['validation']);

					if(is_null($filtered))
					{
						$this->invalid_fields[] = $field;
						unset($this->userdata[$field]);

						continue;
					}

					$this->userdata[$field] = $filtered;
				}
			}

			return((boolean) !sizeof($this->invalid_fields));
		}

		/**
		 * Save method, attempts to validate and save the data 
		 * into the database
		 *
		 * @param	boolean				Whether to rebuild the datastore elements for this datamanager
		 * @return	boolean				Returns true if the data is saved with success, otherwise boolean false
		 *
		 * @throws	Tuxxedo_Basic_Exception		Throws a basic exception if the query should fail
		 * @throws	Tuxxedo_Named_FormData_Exception	Throws a named formdata exception if validation fails
		 */
		public function save($rebuild = true)
		{
			if(!$this->validate())
			{
				$formdata = Array();

				foreach($this->invalid_fields as $field)
				{
					$formdata[$field] = $this->field_names[$field];
				}

				throw new Tuxxedo_Named_Formdata_Exception($formdata);
			}

			$values		= '';
			$sql 		= 'REPLACE INTO `' . $this->tablename . '` (';
			$virtual	= array_merge($this->data, $this->userdata);
			$virtual	= (!is_null($this->identifier) ? array_merge(Array($this->idname => $this->identifier), $virtual) : $virtual);
			$n 		= sizeof($virtual);

			foreach($virtual as $field => $data)
			{
				if(isset($this->fields[$field]['validation']) && $this->fields[$field]['validation'] == self::VALIDATE_STRING)
				{
					$data = htmlspecialchars($data, ENT_QUOTES);
				}

				$sql 	.= '`' . $field . '`' . (--$n ? ', ' : '');
				$values .= '\'' . $this->tuxxedo->db->escape($data) . '\'' . ($n ? ', ' : '');
			}

			if(!$this->tuxxedo->db->query($sql . ') VALUES (' . $values . ')'))
			{
				return(false);
			}

			if($rebuild && method_exists($this, 'rebuild'))
			{
				return($this->rebuild($this->tuxxedo, $virtual));
			}

			return(true);
		}
	}
?>